Adding metal kernels for the gated delta nets. - #4020
Conversation
5810e68 to
8861d85
Compare
|
Hi @tpegolotti — thanks for this PR, and thanks @zcbenz for pointing me at it. The chunked WY-form kernels are a clear step up for batched prefill (the B ≥ 4 curves speak for themselves), and I'd be glad to see this land. @zcbenz asked me to look for possible improvements, so here are my notes. Everything below comes from reading the diff — I have not run the branch yet — so please take it as suggestions and correct me wherever I've misread something. I'm happy to help with any of it (patches against this branch, or measurements on an M5 Max).
Again, happy to turn any of these into patches or measurements — just say which would be most useful. (Also glad to help with the rebase onto current main if that's useful.) |
|
Hi @wyanzhao, thanks for the comments and suggestions! I've added most of them in the latest commits.
|
|
Thanks for the invitation — the packed-seq PR is up at #4409. Short version: bitwise-equal by construction; sequential is 1.86–2.16× One heads-up: this makes the sequential denominator of your speedup I left two pre-existing items out of that PR: |
Co-authored-by: Cheng <git@zcbenz.com>
24ce3d9 to
60af0df
Compare
zcbenz
left a comment
There was a problem hiding this comment.
This is awesome! Just some nitpickings.
| gm = mx.where(mask[..., None], g, 1.0) | ||
|
|
||
| out_ref, hf_ref = mx.fast.gated_delta_update( | ||
| qm, km, vm, gm, bm, initial_state=h0, stream=mx.cpu |
There was a problem hiding this comment.
The tests run too slow, for daily development we run full test suite quite often so a single test should be very fast. For testing fallbacks we usually choose to use shapes not supported by fused kernels rather than forcing cpu streams.
|
|
||
| def test_gated_delta_dtypes(self): | ||
| dtypes = [mx.bfloat16, mx.float32] | ||
| streams = [mx.cpu, mx.gpu] if mx.is_available(mx.gpu) else [mx.cpu] |
There was a problem hiding this comment.
The CI runs separately for CPU and GPU builds so we don't need to manually setup streams in each test, for most cases you can use the default stream and expect CI to test all.
| out, hf = mx.fast.gated_delta_update( | ||
| q, k, v, g, b, initial_state=h0, stream=stream | ||
| ) | ||
| mx.eval(out, hf) |
There was a problem hiding this comment.
There is not need to eval the outputs when merely testing dtypes.
|
|
||
| int C = 1; | ||
| const char* threashold_env = std::getenv("GATED_DELTA_THRESH"); | ||
| int threshold = threashold_env ? std::stoi(threashold_env) : 16; |
There was a problem hiding this comment.
There is a utility function for this:
int threshold = env::get_var("GATED_DELTA_THRESH", 16);| if (!metal::is_nax_available()) | ||
| C = std::min(C, 8); // override in case nax is not available. | ||
|
|
||
| std::string suffix = get_type_string(q.dtype()) + "_" + std::to_string(Dk) + |
There was a problem hiding this comment.
There is a concatenate utility:
std::string suffix;
concatenate(suffix, get_type_string(q.dtype()), "_", Dk, ...);which is preferred than a + b + c because the latter creates a lot temporary strings which is extremely inefficient in C++.
| compute_encoder.set_output_array(hf, 7); // final state out | ||
| compute_encoder.set_bytes(T, 8); | ||
|
|
||
| auto grid = MTL::Size(32, Dv / 8, B * Hv); |
There was a problem hiding this comment.
It seems that the code for C=16/8 are quite similar so we can do:
case 16:
case 8: {
...
auto grid = MTL::Size(32, Dv / C, B * Hv);| @@ -0,0 +1,42 @@ | |||
| #include "mlx/backend/metal/kernels/gated_delta_update_impl.h" | |||
There was a problem hiding this comment.
We don't add _impl in filenames unless it is shared header included by many implementations.
| #include "mlx/backend/metal/kernels/gated_delta_update_impl.h" | |
| #include "mlx/backend/metal/kernels/gated_delta_update.h" |
| #include <MetalPerformancePrimitives/MetalPerformancePrimitives.h> | ||
| #include <metal_tensor> | ||
|
|
||
| #define FULL_UNROLL _Pragma("clang loop unroll(full)") |
There was a problem hiding this comment.
There is already a MLX_MTL_PRAGMA_UNROLL doing the same thing.
Adding kernel support for the Gated Delta Net rule. The math follows the ICLR paper: https://arxiv.org/abs/2412.06464.
Changes
Added three kernels for the forward gated delta rule:
Added benchmark and test scripts. The test script also compares against the naive FLA implementation of the gated delta rule using PyTorch ops.
Models Tested
Additionally to the tests added, I validated the kernels by running
mlx_lm.evaluate --model <model> --tasks wikitext --num-shots 5 --max-tokens 2048on the following modelsmlx-community/Qwen3.5-9B-MLX-4bit,mlx-community/Qwen3.5-35B-A3B-8bit, andmlx-community/Qwen3.5-27B-4bit. In the following table, I report the "word_perplexity" value.As expected, they all match.
Performance
I give micro benchmarks on M1 Max and M5 Max. These can be obtained by running
python benchmarks/python/gated_delta_bench.py.As well as full end to end prompt processing measurements over the 3 models used for validation on the M5 Max.

Future work